﻿using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace NMA.Infrastructure.NHibernate.CriteriaExpression.Category
{
    using NMA.Domain.Model;
    using NMA.Domain.Model.Contract;
    using NMA.Infrastructure.DBC;
    using NMA.Domain.Shared;

    public class SearchCategoryCriteriaExpression : CriteriaExpressionBase<ICategory, CategorySearchCriteria, SearchCategoryCriteriaType>
    {
        // used this for caching
        private static MethodInfo _compareMethod = null;

        public override Func<ICategory, bool> CreatedCriteriaExpression(CategorySearchCriteria criteriaModel, SearchCategoryCriteriaType criteriaType)
        {
            Check.Assert(criteriaModel != null, "CategorySearchCriteria is null");

            Expression<Func<ICategory, bool>> result = null;
            // build global x param, if not build it we have a not global x parametter when execute Lambda Expression
            ParameterExpression paramX = Expression.Parameter(typeof(ICategory), "x");
            Tuple<ParameterExpression, CategorySearchCriteria> parametters = new Tuple<ParameterExpression, CategorySearchCriteria>(paramX, criteriaModel);

            switch (criteriaType)
            {
                case SearchCategoryCriteriaType.Id:
                    result = GetIdExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.Name:
                    result = GetNameExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.CreatedUser:
                    result = GetCreatedByExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.CreatedDate:
                    result = GetDateTimeExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.Id | SearchCategoryCriteriaType.Name:
                    result = GetIdAndNameExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.Id | SearchCategoryCriteriaType.CreatedUser:
                    result = GetIdAndCreatedUserExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.Id | SearchCategoryCriteriaType.CreatedDate:
                    result = GetIdAndCreatedDateExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.Name | SearchCategoryCriteriaType.CreatedUser:
                    result = GetNameAndCreatedUserExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.Name | SearchCategoryCriteriaType.CreatedDate:
                    result = GetNameAndCreatedDateExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.CreatedUser | SearchCategoryCriteriaType.CreatedDate:
                    result = GetCreatedUserAndCreatedDateExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.Id | SearchCategoryCriteriaType.Name | SearchCategoryCriteriaType.CreatedUser:
                    result = GetIdAndNameAndCreatedUserExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.Id | SearchCategoryCriteriaType.Name | SearchCategoryCriteriaType.CreatedDate:
                    result = GetIdAndNameAndCreatedDateExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.Id | SearchCategoryCriteriaType.CreatedUser | SearchCategoryCriteriaType.CreatedDate:
                    result = GetIdAndCreatedUserAndCreatedDateExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.Name | SearchCategoryCriteriaType.CreatedUser | SearchCategoryCriteriaType.CreatedDate:
                    result = GetNameAndCreatedUserAndCreatedDateExpression(parametters);
                    break;
                case SearchCategoryCriteriaType.All:
                    result = GetIdAndNameAndCreatedUserAndCreatedDateExpression(parametters);
                    break;
                default:
                    break;
            }

            return result.Compile();
        }

        #region private functions

        private Expression<Func<ICategory, bool>> GetAllSearchExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            return null;
        }

        private Expression<Func<ICategory, bool>> GetIdExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> result = GetIdExpressionHelper(parametters.Item1, parametters.Item2.Id);

            if (result == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            return BuildLambdaExpression(result);
        }

        private Expression<Func<ICategory, bool>> GetNameExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> result = GetNameExpressionHelper(parametters.Item1, parametters.Item2.Name);

            if (result == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            return BuildLambdaExpression(result);
        }

        private Expression<Func<ICategory, bool>> GetCreatedByExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> result = GetCreatedByExpressionHelper(parametters.Item1, parametters.Item2.CreatedUser);

            if (result == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            return BuildLambdaExpression(result);
        }

        private Expression<Func<ICategory, bool>> GetDateTimeExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> result = GetCreatedDateExpressionHelper(parametters.Item1, parametters.Item2.CreatedDate);

            if (result == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            return BuildLambdaExpression(result);
        }

        private Expression<Func<ICategory, bool>> GetIdAndNameExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> idResult = GetIdExpressionHelper(parametters.Item1, parametters.Item2.Id);
            Tuple<BinaryExpression, ParameterExpression> nameResult = GetNameExpressionHelper(parametters.Item1, parametters.Item2.Name);

            if (idResult == null || nameResult == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            var idAndNameExpr = Expression.And(idResult.Item1, nameResult.Item1);

            // anyway, we can use Item2 in idResult or nameResult
            Tuple<BinaryExpression, ParameterExpression> idAndNameTuple = new Tuple<BinaryExpression, ParameterExpression>(idAndNameExpr, idResult.Item2);

            return BuildLambdaExpression(idAndNameTuple);
        }

        private Expression<Func<ICategory, bool>> GetIdAndCreatedUserExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> idResult = GetIdExpressionHelper(parametters.Item1, parametters.Item2.Id);
            Tuple<BinaryExpression, ParameterExpression> nameResult = GetNameExpressionHelper(parametters.Item1, parametters.Item2.CreatedUser);

            if (idResult == null || nameResult == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            var idAndCreatedUserExpr = Expression.And(idResult.Item1, nameResult.Item1);

            // anyway, we can use Item2 in idResult or nameResult
            Tuple<BinaryExpression, ParameterExpression> idAndCreatedUserTuple = new Tuple<BinaryExpression, ParameterExpression>(idAndCreatedUserExpr, idResult.Item2);

            return BuildLambdaExpression(idAndCreatedUserTuple);
        }

        private Expression<Func<ICategory, bool>> GetIdAndCreatedDateExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> idResult = GetIdExpressionHelper(parametters.Item1, parametters.Item2.Id);
            Tuple<BinaryExpression, ParameterExpression> createdDateResult = GetCreatedDateExpressionHelper(parametters.Item1, parametters.Item2.CreatedDate);

            if (idResult == null || createdDateResult == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            var idAndCreatedDateExpr = Expression.And(idResult.Item1, createdDateResult.Item1);

            // anyway, we can use Item2 in idResult or nameResult
            Tuple<BinaryExpression, ParameterExpression> idAndCreatedDateTuple = new Tuple<BinaryExpression, ParameterExpression>(idAndCreatedDateExpr, idResult.Item2);

            return BuildLambdaExpression(idAndCreatedDateTuple);
        }

        private Expression<Func<ICategory, bool>> GetNameAndCreatedUserExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> nameResult = GetNameExpressionHelper(parametters.Item1, parametters.Item2.Name);
            Tuple<BinaryExpression, ParameterExpression> createdByResult = GetCreatedByExpressionHelper(parametters.Item1, parametters.Item2.CreatedUser);

            if (nameResult == null || createdByResult == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            var nameAndCreatedByExpr = Expression.And(nameResult.Item1, createdByResult.Item1);

            // anyway, we can use Item2 in idResult or nameResult
            Tuple<BinaryExpression, ParameterExpression> nameAndCreatedByTuple = new Tuple<BinaryExpression, ParameterExpression>(nameAndCreatedByExpr, nameResult.Item2);

            return BuildLambdaExpression(nameAndCreatedByTuple);
        }

        private Expression<Func<ICategory, bool>> GetNameAndCreatedDateExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> nameResult = GetNameExpressionHelper(parametters.Item1, parametters.Item2.Name);
            Tuple<BinaryExpression, ParameterExpression> createdDateResult = GetCreatedDateExpressionHelper(parametters.Item1, parametters.Item2.CreatedDate);

            if (nameResult == null || createdDateResult == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            var nameAndCreatedDateExpr = Expression.And(nameResult.Item1, createdDateResult.Item1);

            // anyway, we can use Item2 in idResult or nameResult
            Tuple<BinaryExpression, ParameterExpression> nameAndCreatedDateTuple = new Tuple<BinaryExpression, ParameterExpression>(nameAndCreatedDateExpr, nameResult.Item2);

            return BuildLambdaExpression(nameAndCreatedDateTuple);
        }

        private Expression<Func<ICategory, bool>> GetCreatedUserAndCreatedDateExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> createdByResult = GetCreatedByExpressionHelper(parametters.Item1, parametters.Item2.CreatedUser);
            Tuple<BinaryExpression, ParameterExpression> createdDateResult = GetCreatedDateExpressionHelper(parametters.Item1, parametters.Item2.CreatedDate);

            if (createdByResult == null || createdDateResult == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            var createdUserAndCreatedDateExpr = Expression.And(createdByResult.Item1, createdDateResult.Item1);

            // anyway, we can use Item2 in idResult or nameResult
            Tuple<BinaryExpression, ParameterExpression> createdUserAndCreatedDateTuple = new Tuple<BinaryExpression, ParameterExpression>(createdUserAndCreatedDateExpr, createdByResult.Item2);

            return BuildLambdaExpression(createdUserAndCreatedDateTuple);
        }

        private Expression<Func<ICategory, bool>> GetIdAndNameAndCreatedUserExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> idResult = GetIdExpressionHelper(parametters.Item1, parametters.Item2.Id);
            Tuple<BinaryExpression, ParameterExpression> nameResult = GetNameExpressionHelper(parametters.Item1, parametters.Item2.Name);
            Tuple<BinaryExpression, ParameterExpression> createdByResult = GetCreatedByExpressionHelper(parametters.Item1, parametters.Item2.CreatedUser);

            if (idResult == null || nameResult == null || createdByResult == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            var idAndNameExpr = Expression.And(idResult.Item1, nameResult.Item1);
            var idAndNameAndCreatedByExpr = Expression.And(idAndNameExpr, createdByResult.Item1);

            // anyway, we can use Item2 in idResult or nameResult
            Tuple<BinaryExpression, ParameterExpression> idAndNameAndCreatedByTuple = new Tuple<BinaryExpression, ParameterExpression>(idAndNameAndCreatedByExpr, idResult.Item2);

            return BuildLambdaExpression(idAndNameAndCreatedByTuple);
        }

        private Expression<Func<ICategory, bool>> GetIdAndNameAndCreatedDateExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> idResult = GetIdExpressionHelper(parametters.Item1, parametters.Item2.Id);
            Tuple<BinaryExpression, ParameterExpression> nameResult = GetNameExpressionHelper(parametters.Item1, parametters.Item2.Name);
            Tuple<BinaryExpression, ParameterExpression> createdDateResult = GetCreatedDateExpressionHelper(parametters.Item1, parametters.Item2.CreatedDate);

            if (idResult == null || nameResult == null || createdDateResult == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            var idAndNameExpr = Expression.And(idResult.Item1, nameResult.Item1);
            var idAndNameAndCreatedDateExpr = Expression.And(idAndNameExpr, createdDateResult.Item1);

            // anyway, we can use Item2 in idResult or nameResult
            Tuple<BinaryExpression, ParameterExpression> idAndNameAndCreatedDateTuple = new Tuple<BinaryExpression, ParameterExpression>(idAndNameAndCreatedDateExpr, idResult.Item2);

            return BuildLambdaExpression(idAndNameAndCreatedDateTuple);
        }

        private Expression<Func<ICategory, bool>> GetIdAndCreatedUserAndCreatedDateExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> idResult = GetIdExpressionHelper(parametters.Item1, parametters.Item2.Id);
            Tuple<BinaryExpression, ParameterExpression> createdByResult = GetCreatedByExpressionHelper(parametters.Item1, parametters.Item2.CreatedUser);
            Tuple<BinaryExpression, ParameterExpression> createdDateResult = GetCreatedDateExpressionHelper(parametters.Item1, parametters.Item2.CreatedDate);

            if (idResult == null || createdByResult == null || createdDateResult == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            var idAndCreatedByExpr = Expression.And(idResult.Item1, createdByResult.Item1);
            var idAndCreatedByAndCreatedDateExpr = Expression.And(idAndCreatedByExpr, createdDateResult.Item1);

            // anyway, we can use Item2 in idResult or nameResult
            Tuple<BinaryExpression, ParameterExpression> idAndCreatedByAndCreatedDateTuple = new Tuple<BinaryExpression, ParameterExpression>(idAndCreatedByAndCreatedDateExpr, idResult.Item2);

            return BuildLambdaExpression(idAndCreatedByAndCreatedDateTuple);
        }

        private Expression<Func<ICategory, bool>> GetNameAndCreatedUserAndCreatedDateExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> nameResult = GetNameExpressionHelper(parametters.Item1, parametters.Item2.Name);
            Tuple<BinaryExpression, ParameterExpression> createdByResult = GetCreatedByExpressionHelper(parametters.Item1, parametters.Item2.CreatedUser);
            Tuple<BinaryExpression, ParameterExpression> createdDateResult = GetCreatedDateExpressionHelper(parametters.Item1, parametters.Item2.CreatedDate);

            if (nameResult == null || createdByResult == null || createdDateResult == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            var nameAndCreatedByExpr = Expression.And(nameResult.Item1, createdByResult.Item1);
            var idAndNameAndCreatedDateExpr = Expression.And(nameAndCreatedByExpr, createdDateResult.Item1);

            // anyway, we can use Item2 in idResult or nameResult
            Tuple<BinaryExpression, ParameterExpression> nameAndCreatedByAndCreatedDateTuple = new Tuple<BinaryExpression, ParameterExpression>(idAndNameAndCreatedDateExpr, nameResult.Item2);

            return BuildLambdaExpression(nameAndCreatedByAndCreatedDateTuple);
        }

        private Expression<Func<ICategory, bool>> GetIdAndNameAndCreatedUserAndCreatedDateExpression(Tuple<ParameterExpression, CategorySearchCriteria> parametters)
        {
            Tuple<BinaryExpression, ParameterExpression> idResult = GetIdExpressionHelper(parametters.Item1, parametters.Item2.Id);
            Tuple<BinaryExpression, ParameterExpression> nameResult = GetNameExpressionHelper(parametters.Item1, parametters.Item2.Name);
            Tuple<BinaryExpression, ParameterExpression> createdByResult = GetCreatedByExpressionHelper(parametters.Item1, parametters.Item2.CreatedUser);
            Tuple<BinaryExpression, ParameterExpression> createdDateResult = GetCreatedDateExpressionHelper(parametters.Item1, parametters.Item2.CreatedDate);

            if (idResult == null || nameResult == null || createdByResult == null || createdDateResult == null)
                throw new Exception("Tuple<BinaryExpression, ParameterExpression> object is null");

            var idAndNameExpr = Expression.And(idResult.Item1, nameResult.Item1);
            var idAndNameAndCreatedByExpr = Expression.And(idAndNameExpr, createdByResult.Item1);
            var idAndNameAndCreatedByAndCreatedDateExpr = Expression.And(idAndNameAndCreatedByExpr, createdDateResult.Item1);

            // anyway, we can use Item2 in idResult or nameResult
            Tuple<BinaryExpression, ParameterExpression> idAndNameAndCreatedByAndCreatedDateTuple = new Tuple<BinaryExpression, ParameterExpression>(idAndNameAndCreatedByAndCreatedDateExpr, idResult.Item2);

            return BuildLambdaExpression(idAndNameAndCreatedByAndCreatedDateTuple);
        }

        #endregion

        #region helper functions

        private Expression<Func<ICategory, bool>> BuildLambdaExpression(Tuple<BinaryExpression, ParameterExpression> parametters)
        {
            Check.Assert(parametters != null, "Tuple<BinaryExpression, ParameterExpression> object is null");

            return Expression.Lambda<Func<ICategory, bool>>(parametters.Item1, new ParameterExpression[] { parametters.Item2 });
        }

        private Tuple<BinaryExpression, ParameterExpression> GetIdExpressionHelper(ParameterExpression paramX, Guid id)
        {
            // x.Id
            MemberExpression memberXGuid = Expression.Property(paramX, typeof(IEntity<ICategory>).GetProperty("Id"));

            // constant, for example: "abc"
            ConstantExpression paramConstant = Expression.Constant(id);
            ConstantExpression paramZeroConstant = Expression.Constant(0);

            ParameterExpression paramGuid = Expression.Parameter(typeof(Guid), "Guid");

            // get string.Compare method from string
            var methods = typeof(Guid).GetMethods().Where(x => x.Name.Equals("CompareTo"));

            MethodInfo methodInfo = methods.ElementAt(1);

            var methodCall = Expression.Call(memberXGuid, methodInfo, paramConstant);

            // for example: a == b
            BinaryExpression binaryEx = Expression.Equal(methodCall, paramZeroConstant);

            return new Tuple<BinaryExpression, ParameterExpression>(binaryEx, paramX);
        }

        private Tuple<BinaryExpression, ParameterExpression> GetNameExpressionHelper(ParameterExpression paramX, string name)
        {
            // x.Name
            MemberExpression memberXName = Expression.Property(paramX, typeof(INameEntity<ICategory>).GetProperty("Name"));

            // constant, for example: "abc"
            ConstantExpression paramConstant = Expression.Constant(name);
            ConstantExpression paramZeroConstant = Expression.Constant(0);

            ParameterExpression paramString = Expression.Parameter(typeof(String), "String");

            MethodInfo methodInfo = null;

            if (_compareMethod == null)
            {
                // get string.Compare method from string
                var methods = typeof(String).GetMethods().Where(x => x.Name.Equals("Compare"));

                methodInfo = methods.FirstOrDefault();
                _compareMethod = methodInfo;
            }
            else
            {
                methodInfo = _compareMethod;
            }

            var methodCall = Expression.Call(null, methodInfo, memberXName, paramConstant);

            // for example: a == b
            BinaryExpression binaryEx = Expression.Equal(methodCall, paramZeroConstant);

            return new Tuple<BinaryExpression, ParameterExpression>(binaryEx, paramX);
        }

        private Tuple<BinaryExpression, ParameterExpression> GetCreatedByExpressionHelper(ParameterExpression paramX, string createdBy)
        {
            // x.CreatedUser
            MemberExpression memberXName = Expression.Property(paramX, typeof(ICategory).GetProperty("CreatedUser"));

            // constant, for example: "abc"
            ConstantExpression paramConstant = Expression.Constant(createdBy);
            ConstantExpression paramZeroConstant = Expression.Constant(0);

            ParameterExpression paramString = Expression.Parameter(typeof(String), "String");

            // get string.Compare method from string
            var methods = typeof(String).GetMethods().Where(x => x.Name.Equals("Compare"));

            MethodInfo methodInfo = methods.FirstOrDefault();

            var methodCall = Expression.Call(null, methodInfo, memberXName, paramConstant);

            // for example: a == b
            BinaryExpression binaryEx = Expression.Equal(methodCall, paramZeroConstant);

            return new Tuple<BinaryExpression, ParameterExpression>(binaryEx, paramX);
        }

        private Tuple<BinaryExpression, ParameterExpression> GetCreatedDateExpressionHelper(ParameterExpression paramX, DateTime createdDate)
        {
            // x.CreatedUser
            MemberExpression memberXDateTime = Expression.Property(paramX, typeof(IEntity<ICategory>).GetProperty("CreatedDate"));

            // constant, for example: "abc"
            ConstantExpression paramConstant = Expression.Constant(createdDate);
            ConstantExpression paramZeroConstant = Expression.Constant(0);

            ParameterExpression paramDateTime = Expression.Parameter(typeof(DateTime), "DateTime");

            // get string.Compare method from string
            var methods = typeof(DateTime).GetMethods().Where(x => x.Name.Equals("CompareTo"));

            MethodInfo methodInfo = methods.ElementAt(1);

            var methodCall = Expression.Call(memberXDateTime, methodInfo, paramConstant);

            // for example: a == b
            BinaryExpression binaryEx = Expression.Equal(methodCall, paramZeroConstant);

            return new Tuple<BinaryExpression, ParameterExpression>(binaryEx, paramX);
        }

        #endregion

    }
}
